嗨嗨~大家我是愛搞怪的 YC
一個從小被老師嫌文筆差的人,要來挑戰寫 30 天的技術文章,這件事本身就已經很問號了。
總之這個系列要撬開 Spark、DataFusion 與 Comet 的大門
身為 DataFusion Comet 開源專案的貢獻者,把底層的設計邏輯搞清楚本來就是應該的,與其說是寫給大家看,不如說是我逼自己把「為什麼會長成這樣」一層一層講明白,順便讓大家抓我的錯。
這系列不會有:安裝教學、config 大全、貼一堆程式碼叫你自己看,理由很簡單,API 半年換一輪,寫下來很快就錯了,看了也只是打發時間而已。
那就開始今天的主題:1+1+1 > 3 ~ Spark 與 DataFusion、Comet 效能煉金術 ~
C'est parti !
用個 TPC-H Q1 的簡化版來當栗子🌰:
SELECT l_returnflag, SUM(l_extendedprice * (1 - l_discount)) AS revenue
FROM lineitem
WHERE l_shipdate <= DATE '1998-09-01'
GROUP BY l_returnflag
挑這個當例子是因為它剛好寫了四件分別住在不同層的語句:
WHERE l_shipdate <= DATE '1998-09-01' — Parquet 檔案裡每一塊資料都附帶最小值和最大值。如果某塊的最小 shipdate 已經大於 1998-09-01,那整塊連解碼都不用,直接跳過,這是儲存格式那一層在做的事。l_extendedprice * (1 - l_discount) — 這個乘法減法每一列都得算一次,沒有捷徑可走,所以它是「能不能一道指令同時算八列」這個問題的舞台,這是硬體與向量化那層。SUM(...) — 累加中的結果必須一直留在記憶體,資料量夠大就會放不下,這是查詢引擎那一層的記憶體管理與 spill。GROUP BY l_returnflag — 同一個 returnflag 的資料散在幾百台機器上,要加總就得先集中到同一台,這就是 shuffle,這是跨界那層,也是 Comet 最主要的戰場。TPC-H 是業界標準測試集,資料產生器和查詢都公開,想自己跑一遍驗證都是可以的。
問題是:這句話從送進 Spark 到吐出結果,中間經過哪些地方?
標準答案大概長這樣:parse → analyze → optimize → physical plan → 執行 → 收集結果
六個箱子,教科書都這樣畫,也沒有錯。
問題出在最後兩個箱子,前面四格是三十年查詢處理研究的直接繼承者,論文很多,寫得很清楚。
「執行」那一格則被畫成一個箱子,裡面塞著一整個系統,而這個系列的三十天故事都來自於那一格裡。
把它撬開,會看到一堆沒有標準答案的選擇:
l_shipdate <= '1998-09-01' 這個條件,是誰決定 Parquet 檔案裡哪些 page 要被解碼、哪些可以整塊跳過?SUM 的 hash table 塞不下記憶體的時候,誰讓位、讓多少?五個問號,五個設計決定,而且每一個都有人做過相反的選擇
所以標題那句「你的查詢是這樣也不是這樣」是這個意思:你看到的 plan 是真的,但它只是一層皮。同一句 SQL 底下可以跑出完全不同的東西,而且效能差好幾倍。
順序有五層,由下往上:硬體先講,Comet 排最後。
先說為什麼不反過來
直覺的寫法當然是從 Comet 開始,它是什麼、怎麼裝、有哪些 config,但那樣寫出來會是一份 API 導覽,而且如前面說的,很快就過期 0 人在意的文章。
由下往上的好處是,每個設計都會變成結論而不是規定。先問「現代 CPU 喜歡什麼形狀的資料」,Arrow 的 buffer 佈局就不再是規格,而是一個答案;先問「怎麼做到只讀該讀的位元組」,Parquet 的 encoding 選擇和 Iceberg 的四層 metadata 就變成同一個問題的兩種解法。每一層都在回答上一層留下的問題。
| 層 | 這層在問什麼 |
|---|---|
| 硬體與記憶體 | 為什麼欄式加向量化會比較快,什麼時候不會 |
| 儲存格式 | 怎麼做到只讀該讀的位元組 |
| 查詢引擎 | DataFusion 怎麼把計劃變成執行 |
| 表格式 | Iceberg 與 iceberg-rust 如何接上引擎 |
| 跨界 | Comet 怎麼讓 JVM 和 Rust 共存 |
明天先從最底層開始:NULL 用 bitmask 的 0 還是 1 表示,對單一系統毫無差別,Arrow 為什麼要為這種事立規格?
那就明天見~